Skip to content

gui: the format menu is one width, the Remove button is a button, and macOS gets an icon - #16

Merged
donislawdev merged 2 commits into
mainfrom
gui/the-format-menu-is-one-width-and-the-mac-gets-an-icon
Aug 28, 2026
Merged

gui: the format menu is one width, the Remove button is a button, and macOS gets an icon#16
donislawdev merged 2 commits into
mainfrom
gui/the-format-menu-is-one-width-and-the-mac-gets-an-icon

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Three things the owner reported from the running window on 2026-08-28. In two of the three the cause the measurement found is not the one the report describes.

The format menu was two different widths

140 px on the single batch screen, 98 px on the presets screen and in a row of an archive's contents, for the same twenty formats.

That looked like the screens disagreeing and was an order dependency. parts.menuWidth takes the widest thing a menu can show, which includes its placeholder, and the toolkit substitutes its own default placeholder while the renderer is made - fyne v2.8.1 widget/select.go:94, read in the pinned module. So a menu something had already selected a value on was measured against 76 px of (Select one), and an identical one built cold against 34 px of targz.

The narrow ones then could not show the placeholder they had been given: a fresh row of an archive's contents drew (Select ..., cut off mid word, in the box that exists to show it.

A menu is now never narrower than parts.NumericWidth, the narrowest box these screens draw. That states the report as a relationship rather than as a number - nothing was cut off on the presets screen, and what was wrong is that one setting was 140 px on one screen and 98 on another because of how long the words pdf and targz happen to be.

Rejected on the way, before the commit: a MinSize call before measuring, so the toolkit would have filled its placeholder in first. It cannot change an answer - that string needs 139.91 px and the floor gives 140 - and a line that cannot change an answer is not a defence.

The Remove button was a panel

197.50 x 63.16 px for a word needing 67.92 x 32: a quarter of the form wide and as tall as a label and a control together. parts.Row shares the width out in equal columns, which is what a field wants and what anything else gets whether it wants it or not. parts.BesideFields keeps something that is not a field out of that arithmetic and puts it on the line the controls are on.

The picture of that row has been in testdata/screens since the day the state existed, and the pixel guard compared it on every run. A stored picture proves nothing changed, never that nothing is wrong.

macOS had no icon

The program became a .app in the last release, and a bundle with no icon is drawn by the Finder and the Dock as a blank sheet of paper.

The observation said this was one command on the runner with sips and iconutil, "because the release runner is a Mac anyway". False, and reading the workflow was the first thing done: the command line archives are cross compiled on ubuntu-latest and that is where the bundle is put together. So the icon is a file in the repository, written by tools/appicon.py --icns - ten types, each a PNG drawn at its own size rather than scaled up from one master. Pillow's ICNS writer was tried and is not usable: eight of the ten types, and it upsamples 256 px to 1024.

Verified with Apple's own tool on a real Mac: iconutil --convert iconset exits 0 and hands back all ten entries at the right pixel sizes, and the script was run there end to end on a dummy binary. The script refuses when the icon is missing, because a bundle without one signs, notarises and staples exactly like one with an icon.

Not proven, and it is written down as O156: nobody has seen the icon on a screen. qlmanage over SSH never finishes - Quick Look needs a session with a display, the same wall that leaves D4 open for the window on macOS.

Guards

Five, four proven by mutation and each went red. The fifth reads the icon's committed bytes, so no substitution in a .go or .py file reaches it - it is on provenByProbe, broken by hand three ways (a size cut out, a picture resized, a lying length in the header) and put back byte for byte, checked by hash.

Stored screens regenerated and the diff read: every changed line is a width, except six in recipe-contents.xml which are the two containers now around that button.

Full suite green. New probe tools/probes/menuwidth prints every laid out width per screen, which is what separated the two causes.

donislawdev and others added 2 commits August 28, 2026 20:56
Both reported by the owner from the running window, and in both cases the
cause the measurement found is not the one the report describes.

The format menu was 140 px on the single batch screen and 98 px on the presets
screen and in a row of an archive's contents, for the same twenty formats. That
looked like the screens disagreeing and was an order dependency. parts.menuWidth
takes the widest thing a menu can show, which includes its placeholder, and the
toolkit substitutes its own default placeholder while the renderer is made -
fyne v2.8.1 widget/select.go line 94, read in the pinned module. So a menu that
something had already selected a value on was measured against 76 px of
"(Select one)", and an identical one built cold was measured against 34 px of
"targz". The narrow ones then could not show the placeholder they had been
given, and a fresh row of an archive's contents drew "(Select ...".

A menu is now never narrower than parts.NumericWidth, which is the narrowest
box these screens draw. That is the owner's report stated as a relationship
rather than as a number: nothing was cut off on the presets screen, and what
was wrong is that one setting was 140 px on one screen and 98 on another
because of how long the words "pdf" and "targz" happen to be.

Rejected on the way, before the commit: a call to MinSize before measuring, so
the toolkit would have filled its default placeholder in first. It cannot
change an answer - that string needs 139.91 px and the floor gives 140 - and a
line that cannot change an answer is not a defence.

The Remove button ending a row of an archive's contents was 197.50 x 63.16 px
for a word needing 67.92 x 32: a quarter of the form wide and as tall as a
label and a control together, so it read as a panel with a word in the middle.
parts.Row shares the width out in equal columns, which is what a field wants
and what anything else gets whether it wants it or not. parts.BesideFields
keeps something that is not a field out of that arithmetic and puts it on the
line the controls are on.

Three guards, three mutations, each went red. The stored screens were
regenerated and the diff read: every changed line is a width, except six in
recipe-contents.xml which are the two containers now around that button.

The picture of that row has been in testdata/screens since the day the state
existed, and the pixel guard compared it on every run. A stored picture proves
nothing changed, never that nothing is wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O154, reported by the owner. The program became a .app in the release before
this one, because that is the only shape a notarisation ticket attaches to, and
a bundle with no icon is drawn by the Finder and the Dock as a blank sheet of
paper - which is what a program the system knows nothing about looks like.

The note in that observation said this was one command on the runner with sips
and iconutil, "because the release runner is a Mac anyway". That premise is
false and reading the workflow was the first thing done here: the command line
archives are cross compiled on ubuntu-latest and that is where make_app_bundle
is called, so Apple's tools are not there. Only the window job runs on a Mac.

So the icon is a file in the repository, written by tools/appicon.py --icns.
It assembles the container itself - a header and then one chunk per type, each
holding a PNG drawn at its own size rather than scaled up from one master. Ten
types, because macOS asks for a point size and a scale: 16, 32 twice, 64, 128,
256 twice, 512 twice and 1024. Pillow's own ICNS writer was tried first and is
not usable here: it writes eight of the ten, leaving out the two a screen
without Retina asks for, and it upsamples a 256 px master to 1024.

Verified with Apple's own tool on a real Mac, not assumed: iconutil --convert
iconset exits 0 and hands back all ten entries at those pixel sizes. The script
was then run there end to end on a dummy binary - Resources/icon.icns is in the
bundle and PlistBuddy reads CFBundleIconFile out of the plist.

The script refuses when the icon file is missing rather than carrying on. A
bundle without one builds, signs, notarises and staples exactly like a bundle
with one, so nothing between the build and somebody's screen would say a word.

The rounded square variant, by the owner's decision: every icon in the Dock is
a rounded square of the same size, so a free floating shape reads as smaller
and foreign there. Windows and Linux keep the plain one.

Two guards. The bundle one is proven by mutation. The one that reads the icon's
bytes is proven by probe and says why in the list: it reads a committed binary
asset, so no substitution in a .go or .py file reaches it. Broken by hand three
ways - a size cut out, a picture resized, a lying length in the header - red
each time, and put back byte for byte, checked by hash.

What this does NOT prove, and it is in O156: nobody has seen the icon on a
screen. qlmanage over SSH never finishes, because Quick Look needs a session
with a display, which is the same wall that leaves D4 open for the window on
macOS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit a1c712d into main Aug 28, 2026
18 checks passed
@donislawdev
donislawdev deleted the gui/the-format-menu-is-one-width-and-the-mac-gets-an-icon branch August 28, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant